package edu.umn.cs5115.scheduler.entities;

import edu.umn.cs5115.scheduler.SchedulerDocument;
import edu.umn.cs5115.scheduler.entities.CoursePart;
import edu.umn.cs5115.scheduler.framework.Document;
import edu.umn.cs5115.scheduler.framework.KeyValueAdapter;
import edu.umn.cs5115.scheduler.framework.KeyValueCoding;
import edu.umn.cs5115.scheduler.framework.KeyValueCodingSet;
import edu.umn.cs5115.scheduler.framework.KeyValueObserver;
import edu.umn.cs5115.scheduler.framework.KeyValueObserver.SetMutationType;
import edu.umn.cs5115.scheduler.framework.ManagedData;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

/**
 * @author Grant, Dan
 * 
 *                                      -----------------------------------
 *                                      |        Course                   |
 *                                      |            extends ManagedData  |
 *                                      |            implements CoursePart|
 *                                      |                                 |
 *                                      | - Document = document           |
 *                                      | - getParent() = null            |
 *                                      -----------------------------------
 *                                                        |
 *                                                        |
 *                   --------------------------------------
 *                   |
 *                   |
 *   -----------------------------------
 *   |        Section 1                |
 *   |            extends ManagedData  |
 *   |            implements CoursePart|                      <--- WE ARE HERE
 *   |                                 |
 *   | - Document = Course.document    |------------
 *   | - parentSection = null          |           |
 *   | - getParent() = Course          |           |
 *   -----------------------------------           |
 *                                                 |
 *                                                 |
 *                                ----------------------------------------
 *                                |        Sub Section 1                 |
 *                                |            extends ManagedData       |
 *                                |            implements CoursePart     |
 *                                |                                      |     <---  AND HERE
 *                                | - Document = Section1.Course.document|
 *                                | - parentSection = Section1           |
 *                                | - getParent() = Section1             |
 *                                ----------------------------------------
 */
public class Section extends CoursePart
{
	private String sectionNumber;	// e.g. 002
	private String type;            // e.g. DIS, LEC, LET, etc...
	private KeyValueCodingSet subSections;  // e.g. discussions for a lecture
	private int credits;
	private String staff;
	private KeyValueCodingSet meetingTimes;
	private Section parentSection;  // section this section is a member of the subsections of
	private Course course;          // The Course this is a section of.

	public static final String SECTION_NUMBER_KEY = "sectionNumber";
	public static final String TYPE_KEY           = "type";
	public static final String SUB_SECTIONS_KEY   = "subSections";
	public static final String CREDITS_KEY        = "credits";
	public static final String STAFF_KEY          = "staff";
	public static final String MEETING_TIMES_KEY  = "meetingTimes";
	public static final String PARENT_KEY         = "parent";
	public static final String COURSE_KEY         = "course";
        public static final String CHOSEN_KEY         = "chosen";
        
        private static final HashMap<String, String> sectionTypeNames = new HashMap();

        
        static
        {
            Map defaultMap = new HashMap();
            defaultMap.put(CHOSEN_KEY, Boolean.FALSE);
            setDefaultValues(Section.class, defaultMap);
            sectionTypeNames.put("LEC", "Lecture");
            sectionTypeNames.put("DIS", "Discussion");
            sectionTypeNames.put("LAB", "Lab");
            sectionTypeNames.put("STU", "Studio");
            sectionTypeNames.put("LET", "Lecture Workaround");
            ArrayList sectionsKeyList = new ArrayList(1);
            sectionsKeyList.add(SUB_SECTIONS_KEY);
            setKeysTriggerNotificationForDependentKey(Section.class, sectionsKeyList, CHILDREN_KEY);
        }
        
        /**
         * Create a new section, shared constructor used by both public constructors.
         * @param document The document that this course is created in.
         */
        private Section(Document document)
        {
            super(document);
            meetingTimes = new KeyValueCodingSet();
            subSections = new KeyValueCodingSet();
            
            
            // This will let KeyValueCodingBase know that we have sets on these 
            // keys.
            didChangeValueForKey(MEETING_TIMES_KEY);
            didChangeValueForKey(SUB_SECTIONS_KEY); 
        }
        
	/**
	 * Create a new section.
	 * @param course The course this is a section of.
	 */
	public Section(Course course) {
		this(course.getDocument());
		// connect this object with parents
		this.parentSection = null;
		this.course = course;
		this.course.addSection(this);
	}
	
	/**
	 * Create a new section.
	 * @param parent The section that is the immediate parent of this 
	 * sub-section.
	 */
	public Section(Section parent) {
		this(parent.getDocument());
		// connect this object with parents
		this.parentSection = parent;
		this.course = parent.getCourse();
		parent.addSubSection(this);
	}
	
	/**
	 * @return the meeting times
	 */
	public Set getMeetingTimes() {
		return meetingTimes;
	}

	/**
	 * @return the credits
	 */
	public int getCredits() {
		return credits;
	}

	/**
	 * @return the sectionNumber
	 */
	public String getSectionNumber() {
		return sectionNumber;
	}

	/**
	 * @return the staff
	 */
	public String getStaff() {
		return staff;
	}

	/**
	 * @return the subSections
	 */
	public Set getSubSections() {
		return subSections;
	}

	/**
	 * @return the type
	 */
	public String getType() {
		return type;
	}
        
	/**
	 * Get the parent section.
	 * @return The section that this is the immediate child of.  For 
	 * example, some lectures will have multiple discussion sections 
	 * associated with them.  This will be the course itself for top-level
	 * sections/lectures
	 */
	public CoursePart getParent()
	{
		if (parentSection != null)
			return parentSection;
		else
			return course;
	}
	
	/**
	 * Get the course this section is a part of.
	 * @return The course of this section.
	 */
	public Course getCourse()
	{
		return course;
	}
        
        /**
         * Find out whether or not this section has been chosen.  Chosen 
         * sections are the ones have their checkbox checked and remain on the
         * calendar.
         */
        public boolean isChosen()
        {
            return ((Boolean)getPrimitiveValueForKey(CHOSEN_KEY)).booleanValue();
        }

	/**
	 * @param credits the credits to set
	 */
	public void setCredits(int credits) {
		this.credits = credits;
		didChangeValueForKey(CREDITS_KEY);
	}

	/**
	 * @param meetingTimes the meetingTimes to set
	 */
	public void setMeetingTimes(Collection<MeetingTime> meetingTimes) {
            this.meetingTimes.replace(meetingTimes);
	}

	/**
	 * @param sectionNumber the sectionNumber to set
	 */
	public void setSectionNumber(String sectionNumber) {
		this.sectionNumber = sectionNumber;
		didChangeValueForKey(SECTION_NUMBER_KEY);
	}

	/**
	 * @param staff the staff to set
	 */
	public void setStaff(String staff) {
		this.staff = staff;
		didChangeValueForKey(STAFF_KEY);
	}

	/**
	 * @param subSections the subSections to set
	 */
	private void setSubSections(Collection<Section> subSections) {
		this.subSections.replace(subSections);
	}

	/**
	 * @param type the type to set
	 */
	public void setType(String type) {
		this.type = type;
		didChangeValueForKey(TYPE_KEY);
	}
        
        /**
         * Marks or unmarks the course as being one that is chosen.  Chosen 
         * courses are the ones that have their checkbox checked.
         * @param chosen True to choose (check) this section, false to uncheck
         * it.
         */
	public void setChosen(boolean chosen)
        {
            if (isChosen() != chosen)
            {
                setPrimitiveValueForKey(CHOSEN_KEY, chosen);
                // recalculate status for display
                recalculateStatus();
                if (chosen)
                {
                    // chose all ancestors
                    if (parentSection != null)
                        parentSection.setChosen(true);
                } else {
                    // unchoose all children
                    Collection<CoursePart> children = getChildren();
                    for(CoursePart child : children)
                        ((Section)child).setChosen(false);
                    
                    // notify sections that conflict with us that since there's
                    // no longer a conflict, they should recalculate their status
                    Set<Section> conflicts = TimeConflict.findConflicts(this, ((SchedulerDocument)getDocument()).getSchedule());
                    for(Section conflict : conflicts)
                        conflict.recalculateStatus();
                    
                    // Notify the first other chosen siblings that were in a 
                    // conflict with us, since unchecking us may have resolved 
                    // the conflict.  If there's more than one selected, the 
                    // conflict won't have been resolved; if there's just one
                    // it works just perfectly
                    for(CoursePart sibling : getSiblings())
                    {
                        if (((Section)sibling).isChosen())
                        {
                            sibling.recalculateStatus();
                            break;
                        }
                    }
                }
            }
        }
	
	public void addMeetingTime( MeetingTime time ) {
		meetingTimes.add(time);
		didChangeValueForKey(MEETING_TIMES_KEY);
	}
	
	private void addSubSection( Section child ) {
		subSections.add(child);
		didChangeValueForKey(SUB_SECTIONS_KEY);
	}
	
	/**
	 * Returns the string representation of the section, in the following format:
	 *  001 LEC , 06:30 P.M. - 09:00 P.M. , Th (09/05/2006 - 12/13/2006) , 
	 *  EE/CSci  3-111 , TCEASTBANK , Sturtivant,Carl, 4 credits
	 */
	public String toString() {
		StringBuffer result = new StringBuffer();
		
		// append the section identifier
		result.append( this.getSectionNumber() );
		result.append(" ");
		result.append( this.getType() );
		result.append(", ");
		
		// append the meeting time(s)
		Set<MeetingTime> times = this.getMeetingTimes();
		Iterator<MeetingTime> iter = times.iterator();
		
		if ( times.size() > 1 )
			result.append("\n");
		while ( iter.hasNext() ) {
			result.append(iter.next().toString());
			result.append("\n");
		}
		
		// append staff and credits
		result.append(this.getStaff());
		result.append(", ");
		result.append(this.getCredits());
		result.append(" credits");
		
		if ( this.getSubSections().size() > 0 ) {
			result.append("\n  " + getSubSections().size() + " sub-sections");
		}

		return result.toString();
	}

    /**
     * Checks the descendants of this section to find if any of them are chosen.
     * @return True if some descendent is chosen, false if no descendants are
     * chosen.
     */
    public boolean isAnyDescendantChosen()
    {
        if (isChosen())
            return true;
        for(CoursePart child : getChildren())
        {
            if (child.isAnyDescendantChosen())
                return true;
        }
        return false;
    }
        
    public Collection<CoursePart> getSiblings()
    {
        return getParent().getChildren();
    }

    public Collection<CoursePart> getChildren()
    {
        return subSections;
    }

    public boolean isAncestorOf(CoursePart descendant)
    {
        if (descendant == null)
            return false;
        if (descendant.getCourse() != getCourse())
            return false;
        else {
            CoursePart parent = descendant;
            do
            {
                parent = parent.getParent();
            } while (parent != this && parent != null);
            return (parent == this);
        }
    }

    public boolean isSiblingOf(CoursePart sibling)
    {
        if (sibling != null)
            return (sibling.getCourse() == getCourse() && sibling.getParent() == getParent());
        else
            return false;
    }

    public boolean isChildOf(CoursePart parent)
    {
        return getParent() == parent;
    }

    /**
     * Gets a human readable version of the type.  For example, if the type is
     * LEC, this returns "Lecture".  Please add the codes necessary in the 
     * static initializer to finish support for this.
     * @return A string with a friendly version of the course type in it.
     */
    public String getHumanReadableType()
    {
        String name = sectionTypeNames.get(getType());
        if (name == null)
            return getType();
        else
            return name;
    }
    
    /**
     * Called by recalculate status when there is a time conflict with another
     * course.  This sets that course's status quickly, without doing 
     * calculations, to avoid looping infinitely.
     * @param conflictingSection The section which has a time conflict with this
     * section.
     */
    private void setTimeConflict(Section conflictingSection)
    {
        setStatus(Status.CONFLICT_TIME, "There is a scheduling conflict with " + conflictingSection.getCourse().getShortName());
    }
    
    /**
     * Called by recalculate status when there is a multiple section conflict
     * with another section in the same course.  This sets that course's status
     * quickly, without doing other calculations, to avoid looping infinitely.
     */
    private void setMultipleSectionConflict()
    {
        setStatus(Status.CONFLICT_MULTIPLE, "You must select only one " + getHumanReadableType() + " section");
    }
    
    /**
     * Recalculates the status and message.
     */
    protected void recalculateStatus()
    {
        Status newStatus = Status.NONE;
        String newStatusBrief = "";
        if (isChosen())
        {
            // first check to see if there are multiple selections at this level
            Collection<CoursePart> siblings = getSiblings();
            for(CoursePart sibling : siblings)
            {
                if (sibling != this && ((Section)sibling).isChosen())
                {
                    ((Section)sibling).setMultipleSectionConflict();
                    newStatus = Status.CONFLICT_MULTIPLE;
                    newStatusBrief = "You must select only one " + getHumanReadableType() + " section";
                }
            }
            
            // next check for a scheduling conflict.
            Set<Section> conflictingSections = TimeConflict.findConflicts(this, ((SchedulerDocument)getDocument()).getSchedule());
            if (!conflictingSections.isEmpty())
            {
                newStatus = Status.CONFLICT_TIME;
                if (conflictingSections.size() == 1)
                {
                    newStatusBrief = "There is a scheduling conflict with " + conflictingSections.iterator().next().getCourse().getShortName();
                } else {
                    newStatusBrief = "There are multiple scheduling conflicts";
                }
                
                for(Section conflict : conflictingSections)
                    conflict.setTimeConflict(this);
            }
            
            if (newStatus == Status.NONE)
            {
                if (subSections.size() > 0)
                {
                    Iterator iterator = subSections.iterator();
                    Section chosenSection = null;

                    while(iterator.hasNext() && newStatus == Status.NONE)
                    {
                        Section section = (Section)iterator.next();
                        newStatus = section.getStatus();
                        switch (newStatus)
                        {
                            case CONFLICT_TIME:
                                newStatusBrief = section.getStatusBrief();
                                break;
                            case CONFLICT_MULTIPLE:
                                newStatusBrief = section.getStatusBrief();
                                break;
                            case CONFLICT_MISSING:
                                newStatusBrief = section.getStatusBrief();
                            case COMPLETE:
                                newStatusBrief = "Ready to register for this section";
                                break;
                            case NONE:
                            default:
                                break;
                        }
                    }
                    // if no children are chosen, we won't have a status yet.
                    if (newStatus == Status.NONE)
                    {
                        newStatus = Status.CONFLICT_MISSING;
                        newStatusBrief = "You must select a " + ((Section)subSections.iterator().next()).getHumanReadableType() + " section";
                    }
                } else {
                    newStatus = Status.COMPLETE;
                    newStatusBrief = "Ready to register for this section";
                }
            }
        }
        
        setStatus(newStatus, newStatusBrief);
    }
    
}


